# fscrypt_bash_completion
#
# Copyright 2017 Google Inc.
# Author: Henry-Joseph Audéoud (h.audeoud@gmail.com)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

#
# bash completion scripts require exercising some unusual shell script
# features/quirks, so we have to disable some shellcheck warnings:
#
# Disable SC2016 ("Expressions don't expand in single quotes, use double quotes
# for that") because the 'compgen' built-in expands the argument passed to -W,
# so that argument *must* be single-quoted to avoid command injection.
# shellcheck disable=SC2016
#
# Disable SC2034 ("{Variable} appears unused. Verify use (or export if used
# externally)") because of the single quoting mentioned above as well as the
# fact that we have to declare "local" variables used only by a called function
# (_init_completion()) and not by the function itself.
# shellcheck disable=SC2034
#
# Disable SC2207 ("Prefer mapfile or read -a to split command output (or quote
# to avoid splitting)") because bash completion scripts conventionally use
# COMPREPLY=($(...)) assignments.
# shellcheck disable=SC2207
#
true  # To apply the above shellcheck directives to the entire file


# Generate the completion list for possible mountpoints.
#
# We need to be super careful here because mountpoints can contain whitespace
# and shell meta-characters.  To avoid most problems, we do the following:
#
#   1.) To avoid parsing ambiguities, 'fscrypt status' replaces the space, tab,
#       newline, and backslash characters with octal escape sequences -- like
#       what /proc/self/mountinfo does.  To properly process its output, we need
#       to split lines on space only (and not on other whitespace which might
#       not be escaped), and unescape these characters.  Exception: we don't
#       unescape newlines, as we need to reserve newline as the separator for
#       the words passed to compgen.  (This causes mountpoints containing
#       newlines to not be completed correctly, which we have to tolerate.)
#
#   2.) We backslash-escape all shell meta-characters, and single-quote the
#       argument passed to compgen -W.  Without either step, command injection
#       would be possible.  Without both steps, completions would be incorrect.
#       The list of shell meta-characters used comes from that used by the
#       completion script for umount, which has to solve this same problem.
#
_fscrypt_compgen_mountpoints()
{
    local IFS=$'\n'
    compgen -W '$(_fscrypt_mountpoints_internal)' -- "${cur}"
}

_fscrypt_mountpoints_internal()
{
    fscrypt status 2>/dev/null | command awk -F " " \
    'substr($0, 1, 1) == "/" && $5 == "Yes" {
        gsub(/\\040/, " ", $1)
        gsub(/\\011/, "\t", $1)
        gsub(/\\134/, "\\", $1)
        gsub(/[\]\[(){}<>",:;^&!$=?`|'\''\\ \t\f\n\r\v]/, "\\\\&", $1)
        print $1
    }'
}

# Complete with all possible mountpoints
_fscrypt_complete_mountpoint()
{
    COMPREPLY=($(_fscrypt_compgen_mountpoints))
}


# Output list of possible policy or protector IDs
# $1: the mount point on which policies are looked for.
# $2: the section (policy or protector) to retrieve
_fscrypt_status_section()
{
    local section=${2^^}
    fscrypt status "$1" 2>/dev/null | \
        command awk '/^[[:xdigit:]]{16}/ && section == "'"$section"'" { print $1; next; }
                     { section = $1 }'
}


# Complete with policies or protectors
_fscrypt_complete_policy_or_protector()
{
    local status_section="$1"
    if [[ $cur = *:* ]]; then
        # Complete with IDs of the given mountpoint
        local mountpoint="${cur%:*}" id="${cur#*:}"
        # Note: compgen expands the argument to -W, so it *must* be single-quoted.
        COMPREPLY=($(compgen \
            -W '$(_fscrypt_status_section "${mountpoint}" "${status_section}")' \
            -- "${id}"))
    else
        # Complete with mountpoints, with colon and without ending space
        COMPREPLY=($(_fscrypt_compgen_mountpoints | sed s/\$/:/))
        compopt -o nospace
    fi
}


# Complete with all arguments of that function
_fscrypt_complete_word()
{
    # Note: compgen expands the argument to -W, so it *must* be single-quoted.
    COMPREPLY=($(compgen -W '$*' -- "${cur}"))
}


# Complete with all arguments of that function, plus global options
_fscrypt_complete_option()
{
    local additional_opts=( "$@" )
    # Add global options, always correct
    additional_opts+=( --verbose --quiet --help )
    # Note: compgen expands the argument to -W, so it *must* be single-quoted.
    COMPREPLY=($(compgen -W '${additional_opts[*]}' -- "${cur}"))
}


_fscrypt()
{
    # Initialize completion: compute some local variables to easily
    # detect what is written on the command line.  -s is for splitting
    # long options on `=`, and -n is for splitting them also on `:`
    # (used in the protectors/policies `MOUNTPOINT:ID` forms).
    #
    # `split` is set by `_init_completion -s`, we must declare it local
    # even if we don't use it, not to modify the environment.
    local cur prev words cword split
    _init_completion -s -n : || return

    # Complete the options with argument here, if previous word were such
    # an option.  It would be too difficult to check if they take place in
    # the correct command (such as `fscrypt status # --key ...`)—and that
    # is the command's job—so just complete them first.
    case $prev in
        --key)
            # Any file is accepted
            _filedir
            return ;;
        --name)
            # New value, nothing to complete
            return ;;
        --policy|--protector|--unlock-with)
            local p_or_p="${prev#--}"
            [[ $p_or_p = unlock-with ]] && p_or_p=protector
            _fscrypt_complete_policy_or_protector "${p_or_p}"
            return ;;
        --source)
            # Complete with keywords
            _fscrypt_complete_word \
                pam_passphrase custom_passphrase raw_key
            return ;;
        --time)
            # It's a time, hard to complete a number…
            return ;;
        --user)
            # Complete with a user
            COMPREPLY=($(compgen -u -- "${cur}"))
            return ;;
    esac

    # Fetch positional arguments (i.e. subcommands)
    local positional
    positional=()
    local iword
    for ((iword = 1; iword < ${#words[@]} - 1; iword++)); do
        [[ ${words[iword - 1]} == --@(key|name|policy|protector|unlock-with|source|time|user) ]] \
            && continue  # Argument of previous option, skip
        [[ ${words[iword]} == -* ]] && continue  # Option, skip
        positional+=("${words[iword]}")
    done

    # If completing the first positional, complete with all possible commands
    if [[ ${#positional[@]} == 0 ]]; then
        if [[ $cur == -* ]]; then
            _fscrypt_complete_option
        else
            _fscrypt_complete_word \
                encrypt lock metadata purge setup status unlock
        fi
        return
    fi

    # Complete according to that provided
    case ${positional[0]-} in
        encrypt)  # Directory or option
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option \
                    --policy= --unlock-with= --protector= --source= \
                    --user= --name= --key= --skip-unlock --no-recovery
            else
                _filedir -d
            fi ;;
        lock)  # Directory or option
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option --user= --all-users
            else
                _filedir -d
            fi ;;
        purge)  # Mountpoint or options
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option --user= --force
            else
                _fscrypt_complete_mountpoint
            fi ;;
        setup)  # Mountpoint or options
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option --time= --force
            else
                _fscrypt_complete_mountpoint
            fi ;;
        status)  # Directory (only global options for this command)
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option
            else
                _filedir -d
            fi ;;
        unlock)  # Directory or option
            if [[ $cur == -* ]]; then
                _fscrypt_complete_option --unlock-with= --user= --key=
            else
                _filedir -d
            fi ;;
        metadata)
            # This command has subcommands
            if [[ ${#positional[@]} = 1 ]]; then
                if [[ $cur = -* ]]; then
                    _fscrypt_complete_option
                else
                    # Still no subcommand, complete with them
                    _fscrypt_complete_word \
                        add-protector-to-policy create change-passphrase \
                        destroy dump remove-protector-from-policy
                fi
                return
            fi
            # We have a subcommand, complete according to it
            case ${positional[1]-} in
                add-protector-to-policy)  # Options only
                    _fscrypt_complete_option \
                        --protector= --policy= --unlock-with= --key=
                    ;;
                change-passphrase)  # Options only
                    _fscrypt_complete_option --protector=
                    ;;
                destroy)  # Mountpoint or option
                    if [[ $cur == -* ]]; then
                        _fscrypt_complete_option \
                            --protector= --policy= --force
                    else
                        _fscrypt_complete_mountpoint
                    fi ;;
                dump)  # Options only
                    _fscrypt_complete_option --protector= --policy=
                    ;;
                remove-protector-from-policy)  # Options only
                    _fscrypt_complete_option \
                        --protector= --policy= --force
                    ;;
                create)
                    # This subcommand has subsubcommands
                    if [[ ${#positional[@]} = 2 ]]; then
                        if [[ $cur = -* ]]; then
                            _fscrypt_complete_option
                        else
                            # Still no subcommand, complete with them
                            _fscrypt_complete_word protector policy
                        fi
                        return
                    fi
                    # We have a subsubcommand, complete according to it
                    case ${positional[2]-} in
                        policy)  # Mountpoint or option
                            if [[ $cur = -* ]]; then
                                _fscrypt_complete_option --protector= --key=
                            else
                                _fscrypt_complete_mountpoint
                            fi ;;
                        protector)  # Mountpoint or option
                            if [[ $cur = -* ]]; then
                                _fscrypt_complete_option \
                                    --source= --name= --key= --user=
                            else
                                _fscrypt_complete_mountpoint
                            fi ;;
                        *)
                            # Unrecognized subsubcommand…  Suppose a new
                            # unknown subsubcommand and complete with
                            # global options only
                            _fscrypt_complete_option
                            ;;
                    esac
                    ;;
                *)
                    # Unrecognized subcommand…  Suppose a new unknown
                    # subcommand and complete with global options only
                    _fscrypt_complete_option
                    ;;
             esac
            ;;
        *)
            # Unrecognized command…  Suppose a new unknown command and
            # complete with global options only
            _fscrypt_complete_option
            ;;
    esac

    # When the sole offered completion is --*=, do not put a space after
    # the equal sign as we wait for the argument value.
    [[ ${#COMPREPLY[@]} == 1 ]] && [[ ${COMPREPLY[0]} == "--"*"=" ]] \
        && compopt -o nospace
} &&
    complete -F _fscrypt fscrypt

# ex: filetype=bash
